package pub.pjoc.util.sign;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;

import java.io.UnsupportedEncodingException;
import java.util.*;

/**
 * Sign utils.
 *
 * @author xiongyingqi
 * @version 2016-08-21 00:25
 */
public abstract class SignUtils {
  public static final String DEFAULT_CHARSET = "UTF-8";
  private static final Logger logger = LoggerFactory.getLogger(SignUtils.class);

  /**
   * Check sign if sign result matches presents string. Default ignore empty value.
   *
   * @param signed       presents signed string.
   * @param params       to sign params.
   * @param unsignedKeys keys that not join sign.
   * @return <code>True<code/> if sign matches. Other wise <code>false<code/>.
   */
  public static boolean checkSign(String signed, Map<String, String> params, Set<String> unsignedKeys, String
      append) {
    return checkSign(signed, params, unsignedKeys, append, DEFAULT_CHARSET);
  }

  /**
   * Check sign if sign result matches presents string. Default ignore empty value.
   *
   * @param signed       presents signed string.
   * @param params       to sign params.
   * @param unsignedKeys keys that not join sign.
   * @param charset      charset.
   * @return <code>True<code/> if sign matches. Other wise <code>false<code/>.
   */
  public static boolean checkSign(String signed, Map<String, String> params, Set<String> unsignedKeys, String
      append, String
                                      charset) {
    return checkSign(signed, params, unsignedKeys, append, charset, true);
  }

  /**
   * Check sign if sign result matches presents string.
   *
   * @param signed           presents signed string.
   * @param params           to sign params.
   * @param unsignedKeys     keys that not join sign.
   * @param charset          Charset.
   * @param ignoreEmptyValue ignore pair when value is empty.
   * @return <code>True<code/> if sign matches. Other wise <code>false<code/>.
   */
  public static boolean checkSign(String signed, Map<String, String> params, Set<String> unsignedKeys, String append,
                                  String charset, boolean ignoreEmptyValue) {
    if (signed == null) {
      return false;
    }
    String sign = sign(params, unsignedKeys, append, charset, ignoreEmptyValue);
    boolean match = sign.equalsIgnoreCase(signed);
    if (!match) {
      logger.info("Mismatch sign! signed: {} ours: {}", signed, sign);
    } else if (logger.isDebugEnabled()) {
      logger.debug("Check sign succeed! signed: {} ours: {}", signed, sign);

    }
    return match;
  }

  /**
   * Sign the params to md5 message.
   *
   * @param params           to signed parameters.
   * @param unsignedKeys     keys that not join the sign params.
   * @param append
   * @param charset          charset of md5.
   * @param ignoreEmptyValue empty value should'nt join sign.   @return md5(upper case)
   */
  public static String sign(Map<String, String> params, Set<String> unsignedKeys, String append, String
      charset, boolean ignoreEmptyValue) {
    Assert.notEmpty(params, "parameters is null!");

    SortedMap<String, String> sortedMap = convertToSignMap(params, unsignedKeys, ignoreEmptyValue);
    String signString = toSignString(sortedMap);
    if (StringUtils.hasText(append)) {
      signString += append;
    }
    if (logger.isDebugEnabled()) {
      logger.debug("Final sign string: {}", signString);
    }
    return md5(signString, charset);
  }

  public static SortedMap<String, String> convertToSignMap(Map<String, String> params, Set<String>
      unsignedKeys, boolean ignoreEmptyValue) {
    Assert.notEmpty(params, "parameters is null!");

    SortedMap<String, String> sortedMap = new TreeMap<String, String>();
    sortedMap.putAll(params);
    if (ignoreEmptyValue) {
      for (Iterator<Map.Entry<String, String>> iterator = sortedMap.entrySet().iterator(); iterator.hasNext(); ) {
        Map.Entry<String, String> entry = iterator.next();
        if (StringUtils.isEmpty(entry.getValue())) {
          iterator.remove();
        }
      }
    }
    if (CollectionUtils.isEmpty(unsignedKeys)) {
      return sortedMap;
    }
    for (String unsignedKey : unsignedKeys) {
      String removed = sortedMap.remove(unsignedKey);
      logger.debug("Removed unsigned key: '{}' with value: {}", unsignedKey, removed);
    }
    return sortedMap;
  }

  public static String toSignString(SortedMap<String, String> params) {
    Assert.notEmpty(params, "parameters is null!");
    StringBuilder builder = new StringBuilder();
    Iterator<Map.Entry<String, String>> iterator = params.entrySet().iterator();
    for (; iterator.hasNext(); ) {
      Map.Entry<String, String> entry = iterator.next();
      // ignore empty string
      if (StringUtils.isEmpty(entry.getKey())) {
        continue;
      }
      builder.append(entry.getKey())
          .append("=")
          .append(entry.getValue());
      if (iterator.hasNext()) {
        builder.append("&");
      }
    }
    String signMessage = builder.toString();
    if (logger.isDebugEnabled()) {
      logger.debug("Returning sign string: \"{}\" by params: {}", signMessage, params);
    }
    return signMessage;
  }

  public static String md5(String signString, String charset) {
    Assert.hasText(signString, "Origin sign is null!");
    try {
      byte[] bytes = signString.getBytes(charset);
      String md5 = DigestUtils.md5DigestAsHex(bytes).toUpperCase();
      if (logger.isDebugEnabled()) {
        logger.debug("Returning md5: {} by sign string: {}", md5, signString);
      }
      return md5;
    } catch (UnsupportedEncodingException e) {
      logger.error("", e);
    }
    return null;
  }
}
